package com.easy.secret.codec;

import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.SecureRandom;

import org.apache.commons.lang3.StringUtils;
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.CryptoException;
import org.bouncycastle.crypto.engines.SM2Engine;
import org.bouncycastle.crypto.generators.ECKeyPairGenerator;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.crypto.params.ECKeyGenerationParameters;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.crypto.params.ParametersWithRandom;
import org.bouncycastle.crypto.signers.SM2Signer;
import org.bouncycastle.math.ec.ECCurve;
import org.bouncycastle.math.ec.ECPoint;
import org.bouncycastle.pqc.math.linearalgebra.ByteUtils;

/**
 * @ProjectName: esay-secret-server
 * @Package: com.easy.secret.codec
 * @ClassName: SM2Codec
 * @Description: SM2国密算法
 * @Author: wangjc
 * @CreateDate: 2023/11/17
 * @UpdateUser: wangjc
 * @UpdateDate: 2023/11/17
 * @UpdateRemark: 更新内容
 * @Version: 1.0
 */
public class SM2Codec {
    private static BigInteger ecc_p = new BigInteger("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF", 16);
    private static BigInteger ecc_a = new BigInteger("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFC", 16);
    private static BigInteger ecc_b = new BigInteger("28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93", 16);
    private static BigInteger ecc_n = new BigInteger("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123", 16);
    private static BigInteger ecc_gx = new BigInteger("32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7", 16);
    private static BigInteger ecc_gy = new BigInteger("BC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0", 16);
    private static ECCurve curve;
    private static ECPoint curve_g;
    private static ECDomainParameters domainParams;
    private String CHART_SET = "UTF-8";
    private ECPrivateKeyParameters priKey;
    private ECPublicKeyParameters pubKey;
    private String toStringType = "base64";
    private static SM2Codec sm2Encoder;

    public SM2Codec() {
    }

    public static SM2Codec instance() {
        if (sm2Encoder == null) {
            sm2Encoder = new SM2Codec();
        }

        return sm2Encoder;
    }

    public static SM2Codec instance(String priKey, String pubKey) {
        sm2Encoder = new SM2Codec();
        if (StringUtils.isNotBlank(priKey)) {
            BigInteger bigInteger = new BigInteger(priKey, 16);
            sm2Encoder.priKey = new ECPrivateKeyParameters(bigInteger, domainParams);
        }

        if (StringUtils.isNotBlank(pubKey)) {
            ECPoint point = domainParams.getCurve().decodePoint(ByteUtils.fromHexString(pubKey));
            sm2Encoder.pubKey = new ECPublicKeyParameters(point, domainParams);
        }

        return sm2Encoder;
    }

    public static SM2Codec instance(String priKey, String pubKey, String toStringType) throws Exception {
        sm2Encoder = new SM2Codec();
        if (StringUtils.isNotBlank(toStringType)) {
            sm2Encoder.toStringType = toStringType;
        }

        if ("hex".equalsIgnoreCase(sm2Encoder.toStringType)) {
            if (StringUtils.isNotBlank(priKey)) {
                BigInteger bigInteger = new BigInteger(priKey, 16);
                sm2Encoder.priKey = new ECPrivateKeyParameters(bigInteger, domainParams);
            }

            if (StringUtils.isNotBlank(pubKey)) {
                ECPoint point = domainParams.getCurve().decodePoint(ByteUtils.fromHexString(pubKey));
                sm2Encoder.pubKey = new ECPublicKeyParameters(point, domainParams);
            }
        } else if (StringUtils.isNotBlank(pubKey)) {
            String realKey = ByteUtils.toHexString(Base64Codec.instance().decodeByte(pubKey, (String)null));
            ECPoint point = domainParams.getCurve().decodePoint(ByteUtils.fromHexString(realKey));
            sm2Encoder.pubKey = new ECPublicKeyParameters(point, domainParams);
        }

        return sm2Encoder;
    }

    public SM2Codec.SM2SecureKey generateKey() {
        ECKeyGenerationParameters ekgParams = new ECKeyGenerationParameters(domainParams, new SecureRandom());
        ECKeyPairGenerator keyGen = new ECKeyPairGenerator();
        keyGen.init(ekgParams);
        AsymmetricCipherKeyPair keyPair = keyGen.generateKeyPair();
        ECPrivateKeyParameters priKey = (ECPrivateKeyParameters)keyPair.getPrivate();
        String priKeyStr = ByteUtils.toHexString(priKey.getD().toByteArray());
        ECPublicKeyParameters pubKey = (ECPublicKeyParameters)keyPair.getPublic();
        String pubKeyStr = ByteUtils.toHexString(pubKey.getQ().getEncoded(false));
        return new SM2Codec.SM2SecureKey(priKeyStr, pubKeyStr);
    }

    public static SM2Codec.SM2SecureKey hexKey() throws Exception {
        ECKeyGenerationParameters ekgParams = new ECKeyGenerationParameters(domainParams, new SecureRandom());
        ECKeyPairGenerator keyGen = new ECKeyPairGenerator();
        keyGen.init(ekgParams);
        AsymmetricCipherKeyPair keyPair = keyGen.generateKeyPair();
        ECPrivateKeyParameters priKey = (ECPrivateKeyParameters)keyPair.getPrivate();
        String priKeyStr = ByteUtils.toHexString(priKey.getD().toByteArray());
        ECPublicKeyParameters pubKey = (ECPublicKeyParameters)keyPair.getPublic();
        String pubKeyStr = ByteUtils.toHexString(pubKey.getQ().getEncoded(false));
        return new SM2Codec.SM2SecureKey(priKeyStr, pubKeyStr);
    }

    public static SM2Codec.SM2SecureKey baseKey() throws Exception {
        ECKeyGenerationParameters ekgParams = new ECKeyGenerationParameters(domainParams, new SecureRandom());
        ECKeyPairGenerator keyGen = new ECKeyPairGenerator();
        keyGen.init(ekgParams);
        AsymmetricCipherKeyPair keyPair = keyGen.generateKeyPair();
        ECPrivateKeyParameters priKey = (ECPrivateKeyParameters)keyPair.getPrivate();
        ECPublicKeyParameters pubKey = (ECPublicKeyParameters)keyPair.getPublic();
        return new SM2Codec.SM2SecureKey(Base64Codec.instance().encode(priKey.getD().toByteArray()), Base64Codec.instance().encode(pubKey.getQ().getEncoded(false)));
    }

    public String encrypt(String publicKey, String cipherData) throws Exception {
        return this.encrypt(publicKey, cipherData, this.CHART_SET);
    }

    public String encrypt(String publicKey, String cipherData, String charset) throws Exception {
        ECDomainParameters domainParams = SM2Codec.domainParams;
        ECPoint point;
        ECPublicKeyParameters ecpKey;
        byte[] encData;
        if ("hex".equalsIgnoreCase(this.toStringType)) {
            point = domainParams.getCurve().decodePoint(ByteUtils.fromHexString(publicKey));
            ecpKey = new ECPublicKeyParameters(point, domainParams);
            encData = encrypt(ecpKey, cipherData.getBytes(charset));
            return ByteUtils.toHexString(encData);
        } else {
            point = domainParams.getCurve().decodePoint(Base64Codec.instance().decodeByte(publicKey, (String)null));
            ecpKey = new ECPublicKeyParameters(point, domainParams);
            encData = encrypt(ecpKey, cipherData.getBytes(charset));
            return Base64Codec.instance().encode(encData);
        }
    }

    public static byte[] encrypt(ECPublicKeyParameters pubKey, byte[] cipherData) throws CryptoException {
        SM2Engine engine = new SM2Engine();
        ParametersWithRandom pwr = new ParametersWithRandom(pubKey, new SecureRandom());
        engine.init(true, pwr);
        return engine.processBlock(cipherData, 0, cipherData.length);
    }

    public String decrypt(String privateKey, String cipherData) throws CryptoException, UnsupportedEncodingException {
        return this.decrypt(privateKey, cipherData, this.CHART_SET);
    }

    public String decrypt(String privateKey, String cipherData, String charset) throws CryptoException, UnsupportedEncodingException {
        if ("hex".equalsIgnoreCase(this.toStringType)) {
            BigInteger bigInteger = new BigInteger(privateKey, 16);
            ECPrivateKeyParameters ecpKey = new ECPrivateKeyParameters(bigInteger, domainParams);
            return new String(this.decrypt(ecpKey, ByteUtils.fromHexString(cipherData)), charset);
        } else {
            String realKey = ByteUtils.toHexString(Base64Codec.instance().decodeByte(privateKey, (String)null));
            BigInteger bigInteger = new BigInteger(realKey, 16);
            ECPrivateKeyParameters ecpKey = new ECPrivateKeyParameters(bigInteger, domainParams);
            return new String(this.decrypt(ecpKey, Base64Codec.instance().decodeByte(cipherData, (String)null)), charset);
        }
    }

    public byte[] decrypt(ECPrivateKeyParameters priKey, byte[] cipherData) throws CryptoException {
        SM2Engine engine = new SM2Engine();
        engine.init(false, priKey);
        return engine.processBlock(cipherData, 0, cipherData.length);
    }

    public String sign(String cipherData) throws CryptoException, UnsupportedEncodingException {
        byte[] data = cipherData.getBytes(this.CHART_SET);
        return "hex".equalsIgnoreCase(this.toStringType) ? ByteUtils.toHexString(this.sign(this.priKey, data)) : Base64Codec.instance().encode(this.sign(this.priKey, data));
    }

    public byte[] sign(byte[] cipherData) throws CryptoException, UnsupportedEncodingException {
        return this.sign(this.priKey, cipherData);
    }

    public String sign(String cipherData, String chartSet) throws CryptoException, UnsupportedEncodingException {
        byte[] data = cipherData.getBytes(StringUtils.isBlank(chartSet) ? this.CHART_SET : chartSet);
        return "hex".equalsIgnoreCase(this.toStringType) ? ByteUtils.toHexString(this.sign(this.priKey, data)) : Base64Codec.instance().encode(this.sign(this.priKey, data));
    }

    public String sign(String privateKey, String cipherData, String chartSet) throws CryptoException, UnsupportedEncodingException {
        byte[] data = cipherData.getBytes(StringUtils.isBlank(chartSet) ? this.CHART_SET : chartSet);
        if ("hex".equalsIgnoreCase(this.toStringType)) {
            BigInteger bigInteger = new BigInteger(privateKey, 16);
            ECPrivateKeyParameters ecpKey = new ECPrivateKeyParameters(bigInteger, domainParams);
            return ByteUtils.toHexString(this.sign(ecpKey, data));
        } else {
            String realKey = ByteUtils.toHexString(Base64Codec.instance().decodeByte(privateKey, (String)null));
            BigInteger bigInteger = new BigInteger(realKey, 16);
            ECPrivateKeyParameters ecpKey = new ECPrivateKeyParameters(bigInteger, domainParams);
            return Base64Codec.instance().encode(this.sign(ecpKey, data));
        }
    }

    public byte[] sign(ECPrivateKeyParameters privateKey, byte[] data) throws CryptoException {
        SM2Signer signer = new SM2Signer();
        ParametersWithRandom pwr = new ParametersWithRandom(privateKey, new SecureRandom());
        signer.init(true, pwr);
        signer.update(data, 0, data.length);
        return signer.generateSignature();
    }

    public boolean verify(String data, String sign) throws UnsupportedEncodingException {
        return "hex".equalsIgnoreCase(this.toStringType) ? this.verify(this.pubKey, data.getBytes(this.CHART_SET), ByteUtils.fromHexString(sign)) : this.verify(this.pubKey, data.getBytes(this.CHART_SET), Base64Codec.instance().decodeByte(sign, (String)null));
    }

    public boolean verify(byte[] data, byte[] sign) throws UnsupportedEncodingException {
        return this.verify(this.pubKey, data, sign);
    }

    public boolean verify(String data, String sign, String chartSet) throws UnsupportedEncodingException {
        return "hex".equalsIgnoreCase(this.toStringType) ? this.verify(this.pubKey, data.getBytes(StringUtils.isBlank(chartSet) ? this.CHART_SET : chartSet), ByteUtils.fromHexString(sign)) : this.verify(this.pubKey, data.getBytes(StringUtils.isBlank(chartSet) ? this.CHART_SET : chartSet), Base64Codec.instance().decodeByte(sign, (String)null));
    }

    public boolean verify(String publicKey, String data, String sign, String chartSet) throws UnsupportedEncodingException {
        if ("hex".equalsIgnoreCase(this.toStringType)) {
            ECPoint point = domainParams.getCurve().decodePoint(ByteUtils.fromHexString(publicKey));
            return this.verify(new ECPublicKeyParameters(point, domainParams), data.getBytes(StringUtils.isBlank(chartSet) ? this.CHART_SET : chartSet), ByteUtils.fromHexString(sign));
        } else {
            String realKey = ByteUtils.toHexString(Base64Codec.instance().decodeByte(publicKey, (String)null));
            ECPoint point = domainParams.getCurve().decodePoint(ByteUtils.fromHexString(realKey));
            return this.verify(new ECPublicKeyParameters(point, domainParams), data.getBytes(StringUtils.isBlank(chartSet) ? this.CHART_SET : chartSet), Base64Codec.instance().decodeByte(sign, (String)null));
        }
    }

    public boolean verify(ECPublicKeyParameters pubKey, byte[] data, byte[] sign) {
        SM2Signer signer = new SM2Signer();
        signer.init(false, pubKey);
        signer.update(data, 0, data.length);
        return signer.verifySignature(sign);
    }

    public String getToStringType() {
        return this.toStringType;
    }

    public void setToStringType(String toStringType) {
        this.toStringType = toStringType;
    }

    static {
        curve = new ECCurve.Fp(ecc_p, ecc_a, ecc_b, (BigInteger)null, (BigInteger)null);
        curve_g = curve.createPoint(ecc_gx, ecc_gy);
        domainParams = new ECDomainParameters(curve, curve_g, ecc_n, BigInteger.ONE);
    }

    public static class SM2SecureKey {
        private String priKey;
        private String pubKey;

        public SM2SecureKey() {
        }

        public SM2SecureKey(String priKey, String pubKey) {
            this.priKey = priKey;
            this.pubKey = pubKey;
        }

        public String getPriKey() {
            return this.priKey;
        }

        public void setPriKey(String priKey) {
            this.priKey = priKey;
        }

        public String getPubKey() {
            return this.pubKey;
        }

        public void setPubKey(String pubKey) {
            this.pubKey = pubKey;
        }
    }
}
